<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="Wuh0t" id="Wuh0t"><span data-lake-id="uf9c53962" id="uf9c53962">典型回答</span></h1>
  <p data-lake-id="u416b4553" id="u416b4553"><br></p>
  <p data-lake-id="u23c0e796" id="u23c0e796"><span data-lake-id="u751778fc" id="u751778fc">《Effective Java》一书中，明确表达过一种观点：</span></p>
  <p data-lake-id="u13241097" id="u13241097"><br></p>
  <blockquote data-lake-id="u4f54b030" id="u4f54b030">
   <p data-lake-id="u196e7123" id="u196e7123"><span data-lake-id="ub361ad24" id="ub361ad24">使用枚举实现单例的方法虽然还没有广泛采用，但是单元素的枚举类型已经成为实现Singleton的最佳方法。</span></p>
  </blockquote>
  <p data-lake-id="u8c7a97de" id="u8c7a97de"><span data-lake-id="u8daff41e" id="u8daff41e">​</span><br></p>
  <p data-lake-id="uffcd632f" id="uffcd632f"><span data-lake-id="u784b8a65" id="u784b8a65">究其原因，主要有以下三个好处：</span></p>
  <p data-lake-id="u835aeb34" id="u835aeb34"><span data-lake-id="ue464aeff" id="ue464aeff">1、枚举实现的单例写法简单</span></p>
  <p data-lake-id="u0c5dba9f" id="u0c5dba9f"><span data-lake-id="u87c7b675" id="u87c7b675">2、枚举实现的单例天然是线程安全的</span></p>
  <p data-lake-id="u73881c37" id="u73881c37"><span data-lake-id="ucc9fcaaa" id="ucc9fcaaa">3、枚举实现的单例可避免被反序列化破坏</span></p>
  <p data-lake-id="ue85c7d16" id="ue85c7d16"><span data-lake-id="ue4dfb9cf" id="ue4dfb9cf">​</span><br></p>
  <p data-lake-id="ubebd0d3e" id="ubebd0d3e"><span data-lake-id="u23ceca7a" id="u23ceca7a">​</span><br></p>
  <h1 data-lake-id="KDZ0F" id="KDZ0F"><span data-lake-id="uf2470ad6" id="uf2470ad6">扩展知识</span></h1>
  <p data-lake-id="uf21dff94" id="uf21dff94"><br></p>
  <h2 data-lake-id="S8v6F" id="S8v6F"><span data-lake-id="ubf8b5a88" id="ubf8b5a88">枚举单例写法简单</span></h2>
  <p data-lake-id="u2f59b158" id="u2f59b158"><br></p>
  <p data-lake-id="ue13fe4ae" id="ue13fe4ae"><span data-lake-id="ud444d1c7" id="ud444d1c7">如果你看过《</span><a href="https://www.yuque.com/hollis666/vhr2ge/ggh2h0" data-lake-id="u44bde105" id="u44bde105"><span data-lake-id="u9ac9c0e1" id="u9ac9c0e1">单例模式的多种写法</span></a><span data-lake-id="ude6d7b5d" id="ude6d7b5d">》中的实现单例的所有方式的代码，那就会发现，各种方式实现单例的代码都比较复杂。主要原因是在考虑线程安全问题。</span></p>
  <p data-lake-id="u4b1d354b" id="u4b1d354b"><br></p>
  <p data-lake-id="u65cca61a" id="u65cca61a"><span data-lake-id="uf4abab90" id="uf4abab90">我们简单对比下“双重校验锁”方式和枚举方式实现单例的代码。</span></p>
  <p data-lake-id="ubd255aa4" id="ubd255aa4"><br></p>
  <p data-lake-id="uebe54922" id="uebe54922"><span data-lake-id="ufe5bcfed" id="ufe5bcfed">“双重校验锁”实现单例：</span></p>
  <p data-lake-id="u3c42d1c3" id="u3c42d1c3"><br></p>
  <pre lang="java"><code>
public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}
</code></pre>
  <p data-lake-id="u9a89868f" id="u9a89868f"><br></p>
  <p data-lake-id="u808ed53d" id="u808ed53d"><span data-lake-id="uc29acfd6" id="uc29acfd6">枚举实现单例：</span></p>
  <p data-lake-id="u09420efa" id="u09420efa"><br></p>
  <pre lang="java"><code>
public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}
</code></pre>
  <p data-lake-id="ubf343a86" id="ubf343a86"><br></p>
  <p data-lake-id="u7203a0ee" id="u7203a0ee"><span data-lake-id="u32edaeb7" id="u32edaeb7">相比之下，你就会发现，枚举实现单例的代码会精简很多。</span></p>
  <p data-lake-id="ueaa45fa0" id="ueaa45fa0"><br></p>
  <p data-lake-id="u673c1b9e" id="u673c1b9e"><span data-lake-id="u961d44a7" id="u961d44a7">上面的双重锁校验的代码之所以很臃肿，是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡，代码难免会写的复杂些。但是，这段代码还是有问题的，因为他无法解决反序列化会破坏单例的问题。</span></p>
  <p data-lake-id="u1f0b6e10" id="u1f0b6e10"><br></p>
  <h2 data-lake-id="D2Uvy" id="D2Uvy"><span data-lake-id="uaa071235" id="uaa071235">枚举可解决线程安全问题</span></h2>
  <p data-lake-id="ub87d6a9b" id="ub87d6a9b"><br></p>
  <p data-lake-id="uedad3c31" id="uedad3c31"><span data-lake-id="ucf39d225" id="ucf39d225">上面提到过。使用非枚举的方式实现单例，都要自己来保证线程安全，所以，这就导致其他方法必然是比较臃肿的。那么，为什么使用枚举就不需要解决线程安全问题呢？</span></p>
  <p data-lake-id="u5a43277b" id="u5a43277b"><br></p>
  <p data-lake-id="u8b301c90" id="u8b301c90"><span data-lake-id="u7746182e" id="u7746182e">其实，并不是使用枚举就不需要保证线程安全，只不过线程安全的保证不需要我们关心而已。也就是说，其实在“底层”还是做了线程安全方面的保证的。</span></p>
  <p data-lake-id="u7721f945" id="u7721f945"><br></p>
  <p data-lake-id="u266932b5" id="u266932b5"><span data-lake-id="ufa64bef5" id="ufa64bef5">那么，“底层”到底指的是什么？</span></p>
  <p data-lake-id="u5e316d49" id="u5e316d49"><br></p>
  <p data-lake-id="u637d9102" id="u637d9102"><span data-lake-id="uf7c3375c" id="uf7c3375c">这就要说到关于枚举的实现了：</span></p>
  <p data-lake-id="u9e84b055" id="u9e84b055"><br></p>
  <p data-lake-id="ufc8dbb07" id="ufc8dbb07"><span data-lake-id="u758263f7" id="u758263f7">定义枚举时使用enum和class一样，是Java中的一个关键字。就像class对应用一个Class类一样，enum也对应有一个Enum类。</span></p>
  <p data-lake-id="uaa302951" id="uaa302951"><br></p>
  <p data-lake-id="u551f506a" id="u551f506a"><span data-lake-id="u7081f7fb" id="u7081f7fb">通过将定义好的枚举</span><a href="http://www.hollischuang.com/archives/58" target="_blank" data-lake-id="u96ee7f26" id="u96ee7f26"><span data-lake-id="ua75efba1" id="ua75efba1">反编译</span></a><span data-lake-id="u5d7206df" id="u5d7206df">，我们就能发现，其实枚举在经过</span><code data-lake-id="u269551b4" id="u269551b4"><span data-lake-id="ucb64cd54" id="ucb64cd54">javac</span></code><span data-lake-id="uc10305b3" id="uc10305b3">的编译之后，会被转换成形如</span><code data-lake-id="u303f0a44" id="u303f0a44"><span data-lake-id="u7878a6a4" id="u7878a6a4">public final class T extends Enum</span></code><span data-lake-id="uc6c13a34" id="uc6c13a34">的定义。</span></p>
  <p data-lake-id="ue1d00424" id="ue1d00424"><br></p>
  <p data-lake-id="uaee3a74e" id="uaee3a74e"><span data-lake-id="uf5a62ab7" id="uf5a62ab7">而且，枚举中的各个枚举项同时通过</span><code data-lake-id="u09ee239f" id="u09ee239f"><span data-lake-id="u7293aaa2" id="u7293aaa2">static</span></code><span data-lake-id="u69f8e24e" id="u69f8e24e">来定义的。如：</span></p>
  <p data-lake-id="ued52060b" id="ued52060b"><br></p>
  <pre lang="java"><code>
public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}
</code></pre>
  <p data-lake-id="ub5cd2e22" id="ub5cd2e22"><br></p>
  <p data-lake-id="ufff8b25b" id="ufff8b25b"><span data-lake-id="ubf9d3610" id="ubf9d3610">反编译后代码为：</span></p>
  <p data-lake-id="u9f277b32" id="u9f277b32"><br></p>
  <pre lang="java"><code>
public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
</code></pre>
  <p data-lake-id="u1c24eb34" id="u1c24eb34"><br></p>
  <p data-lake-id="u9a319a53" id="u9a319a53"><span data-lake-id="uf1d923a0" id="uf1d923a0">了解JVM的类加载机制的朋友应该对这部分比较清楚。</span><code data-lake-id="u5a9bf928" id="u5a9bf928"><span data-lake-id="uab93d8b1" id="uab93d8b1">static</span></code><span data-lake-id="u23b5719c" id="u23b5719c">类型的属性会在类被加载过程中被初始化，当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的（因为虚拟机在加载枚举的类的时候，会使用ClassLoader的loadClass方法，而这个方法使用同步代码块保证了线程安全）。所以，创建一个enum类型是线程安全的。</span></p>
  <p data-lake-id="u6618d253" id="u6618d253"><br></p>
  <p data-lake-id="u02a90ca7" id="u02a90ca7"><span data-lake-id="u78480802" id="u78480802">也就是说，我们定义的一个枚举，在第一次被真正用到的时候，会被虚拟机加载并初始化，而这个初始化过程是线程安全的。而我们知道，解决单例的并发问题，主要解决的就是初始化过程中的线程安全问题。</span></p>
  <p data-lake-id="u09a0e8f9" id="u09a0e8f9"><br></p>
  <p data-lake-id="u7957b21d" id="u7957b21d"><span data-lake-id="u19ff0ef0" id="u19ff0ef0">所以，由于枚举的以上特性，枚举实现的单例是天生线程安全的。</span></p>
  <p data-lake-id="u1840fcc6" id="u1840fcc6"><br></p>
  <h2 data-lake-id="C9M6U" id="C9M6U"><span data-lake-id="ue3985ab2" id="ue3985ab2">枚举可解决反序列化会破坏单例的问题</span></h2>
  <p data-lake-id="ufda680aa" id="ufda680aa"><br></p>
  <p data-lake-id="u731fe85b" id="u731fe85b"><span data-lake-id="u9f8bde24" id="u9f8bde24">前面我们提到过，就是使用双重校验锁实现的单例其实是存在一定问题的，就是这种单例有可能被序列化所破坏，</span></p>
  <p data-lake-id="ucc572f02" id="ucc572f02"><br></p>
  <p data-lake-id="ue38c03cf" id="ue38c03cf"><span data-lake-id="udb61d5d0" id="udb61d5d0">那么，对于序列化这件事情，为什么枚举又有先天的优势了呢？答案可以在</span><a href="https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html#6469" target="_blank" data-lake-id="u12da0af4" id="u12da0af4"><span data-lake-id="u18dfeeac" id="u18dfeeac">Java Object Serialization Specification</span></a><span data-lake-id="udd90d89a" id="udd90d89a"> 中找到答案。其中专门对枚举的序列化做了如下规定：</span></p>
  <p data-lake-id="ub3abb6b0" id="ub3abb6b0"><br></p>
  <p data-lake-id="udc26c8ef" id="udc26c8ef"><img src="http://www.hollischuang.com/wp-content/uploads/2018/06/serialization.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_40%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u39ceeb91" id="u39ceeb91"><br></p>
  <p data-lake-id="u28219d5f" id="u28219d5f"><span data-lake-id="ud978d960" id="ud978d960">大概意思就是：在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中，反序列化的时候则是通过</span><code data-lake-id="u842e2e56" id="u842e2e56"><span data-lake-id="u6e6162b1" id="u6e6162b1">java.lang.Enum</span></code><span data-lake-id="u3a206897" id="u3a206897">的</span><code data-lake-id="u7d286a01" id="u7d286a01"><span data-lake-id="u202e3753" id="u202e3753">valueOf</span></code><span data-lake-id="u02de40f7" id="u02de40f7">方法来根据名字查找枚举对象。同时，编译器是不允许任何对这种序列化机制的定制的，因此禁用了</span><code data-lake-id="u968453df" id="u968453df"><span data-lake-id="ude29a13f" id="ude29a13f">writeObject</span></code><span data-lake-id="u05f559da" id="u05f559da">、</span><code data-lake-id="ud60ef7b6" id="ud60ef7b6"><span data-lake-id="uaa0f93d1" id="uaa0f93d1">readObject</span></code><span data-lake-id="ubecdc951" id="ubecdc951">、</span><code data-lake-id="u038d7a05" id="u038d7a05"><span data-lake-id="uc879fc7f" id="uc879fc7f">readObjectNoData</span></code><span data-lake-id="u040466b6" id="u040466b6">、</span><code data-lake-id="u48ac4250" id="u48ac4250"><span data-lake-id="u64b174cc" id="u64b174cc">writeReplace</span></code><span data-lake-id="u38feb7ca" id="u38feb7ca">和</span><code data-lake-id="u0787cd38" id="u0787cd38"><span data-lake-id="u55b70edc" id="u55b70edc">readResolve</span></code><span data-lake-id="ub30f836d" id="ub30f836d">等方法。</span></p>
  <p data-lake-id="ua144ffba" id="ua144ffba"><br></p>
  <p data-lake-id="u45b6885f" id="u45b6885f"><span data-lake-id="u48cf0c7f" id="u48cf0c7f">普通的Java类的反序列化过程中，会通过反射调用类的默认构造函数来初始化对象。所以，即使单例中构造函数是私有的，也会被反射给破坏掉。由于反序列化后的对象是重新new出来的，所以这就破坏了单例。</span></p>
  <p data-lake-id="ucff5717f" id="ucff5717f"><br></p>
  <p data-lake-id="ua5ce010a" id="ua5ce010a"><span data-lake-id="u7f262c48" id="u7f262c48">但是，枚举的反序列化并不是通过反射实现的。所以，也就不会发生由于反序列化导致的单例破坏问题。</span></p>
 </body>
</html>